{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# City street network orientations\n",
    "\n",
    "Author: [Geoff Boeing](https://geoffboeing.com/)\n",
    "\n",
    "Compare the spatial orientations of city street networks with OSMnx.\n",
    "\n",
    "  - [Overview of OSMnx](http://geoffboeing.com/2016/11/osmnx-python-street-networks/)\n",
    "  - [GitHub repo](https://github.com/gboeing/osmnx)\n",
    "  - [Examples, demos, tutorials](https://github.com/gboeing/osmnx-examples)\n",
    "  - [Documentation](https://osmnx.readthedocs.io/en/stable/)\n",
    "  - [Journal article/citation](http://geoffboeing.com/publications/osmnx-complex-street-networks/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'0.11'"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import datetime\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import osmnx as ox\n",
    "import pandas as pd\n",
    "\n",
    "ox.config(log_console=True, use_cache=True)\n",
    "weight_by_length = False\n",
    "\n",
    "ox.__version__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# define the study sites as label : query\n",
    "places = {'Atlanta'       : 'Atlanta, GA, USA',\n",
    "         #'Boston'        : 'Boston, MA, USA',\n",
    "          'Buffalo'       : 'Buffalo, NY, USA',\n",
    "         #'Charlotte'     : 'Charlotte, NC, USA',\n",
    "         #'Chicago'       : 'Chicago, IL, USA',\n",
    "          'Cleveland'     : 'Cleveland, OH, USA',\n",
    "         #'Dallas'        : 'Dallas, TX, USA',\n",
    "         #'Houston'       : 'Houston, TX, USA',\n",
    "          'Denver'        : 'Denver, CO, USA',\n",
    "         #'Detroit'       : 'Detroit, MI, USA',\n",
    "          'Las Vegas'     : 'Las Vegas, NV, USA',\n",
    "         #'Los Angeles'   : {'city':'Los Angeles', 'state':'CA', 'country':'USA'},\n",
    "         #'Manhattan'     : 'Manhattan, NYC, NY, USA',\n",
    "          'Miami'         : 'Miami, FL, USA',\n",
    "          'Minneapolis'   : 'Minneapolis, MN, USA',\n",
    "         #'Orlando'       : 'Orlando, FL, USA',\n",
    "         #'Philadelphia'  : 'Philadelphia, PA, USA',\n",
    "         #'Phoenix'       : 'Phoenix, AZ, USA',\n",
    "         #'Portland'      : 'Portland, OR, USA',\n",
    "         #'Sacramento'    : 'Sacramento, CA, USA',\n",
    "          'San Francisco' : {'city':'San Francisco', 'state':'CA', 'country':'USA'},\n",
    "         #'Seattle'       : 'Seattle, WA, USA',\n",
    "         #'St Louis'      : 'St. Louis, MO, USA',\n",
    "         #'Tampa'         : 'Tampa, FL, USA',\n",
    "          'Washington'    : 'District of Columbia, USA'}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>geometry</th>\n",
       "      <th>place_name</th>\n",
       "      <th>bbox_north</th>\n",
       "      <th>bbox_south</th>\n",
       "      <th>bbox_east</th>\n",
       "      <th>bbox_west</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>MULTIPOLYGON (((-84.55107 33.72079, -84.55096 ...</td>\n",
       "      <td>Atlanta, Fulton County, Georgia, United States</td>\n",
       "      <td>33.886823</td>\n",
       "      <td>33.647808</td>\n",
       "      <td>-84.289560</td>\n",
       "      <td>-84.551068</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>POLYGON ((-78.91945 42.94717, -78.91913 42.946...</td>\n",
       "      <td>Buffalo, Erie County, New York, United States ...</td>\n",
       "      <td>42.966469</td>\n",
       "      <td>42.826039</td>\n",
       "      <td>-78.795168</td>\n",
       "      <td>-78.919453</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>POLYGON ((-81.87909 41.39641, -81.87906 41.395...</td>\n",
       "      <td>Cleveland, Cuyahoga County, Ohio, United States</td>\n",
       "      <td>41.604436</td>\n",
       "      <td>41.390628</td>\n",
       "      <td>-81.532744</td>\n",
       "      <td>-81.879094</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>POLYGON ((-105.10988 39.62710, -105.10761 39.6...</td>\n",
       "      <td>Denver, Denver County, Colorado, United States...</td>\n",
       "      <td>39.914209</td>\n",
       "      <td>39.614315</td>\n",
       "      <td>-104.599689</td>\n",
       "      <td>-105.109885</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>POLYGON ((-115.41463 36.19463, -115.41453 36.1...</td>\n",
       "      <td>Las Vegas, Clark County, Nevada, United States...</td>\n",
       "      <td>36.380491</td>\n",
       "      <td>36.129554</td>\n",
       "      <td>-115.062066</td>\n",
       "      <td>-115.414628</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>POLYGON ((-80.31976 25.76249, -80.31968 25.762...</td>\n",
       "      <td>Miami, Miami-Dade County, Florida, United Stat...</td>\n",
       "      <td>25.855783</td>\n",
       "      <td>25.709052</td>\n",
       "      <td>-80.139157</td>\n",
       "      <td>-80.319760</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>POLYGON ((-93.32916 44.94142, -93.32914 44.940...</td>\n",
       "      <td>Minneapolis, Hennepin County, Minnesota, Unite...</td>\n",
       "      <td>45.051250</td>\n",
       "      <td>44.890150</td>\n",
       "      <td>-93.193858</td>\n",
       "      <td>-93.329163</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>MULTIPOLYGON (((-123.17382 37.77573, -123.1737...</td>\n",
       "      <td>San Francisco, California, United States of Am...</td>\n",
       "      <td>37.929844</td>\n",
       "      <td>37.640314</td>\n",
       "      <td>-122.280016</td>\n",
       "      <td>-123.173825</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>POLYGON ((-77.11977 38.93428, -77.11900 38.933...</td>\n",
       "      <td>Washington, D.C., United States of America</td>\n",
       "      <td>38.995852</td>\n",
       "      <td>38.791630</td>\n",
       "      <td>-76.909366</td>\n",
       "      <td>-77.119766</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                            geometry  \\\n",
       "0  MULTIPOLYGON (((-84.55107 33.72079, -84.55096 ...   \n",
       "1  POLYGON ((-78.91945 42.94717, -78.91913 42.946...   \n",
       "2  POLYGON ((-81.87909 41.39641, -81.87906 41.395...   \n",
       "3  POLYGON ((-105.10988 39.62710, -105.10761 39.6...   \n",
       "4  POLYGON ((-115.41463 36.19463, -115.41453 36.1...   \n",
       "5  POLYGON ((-80.31976 25.76249, -80.31968 25.762...   \n",
       "6  POLYGON ((-93.32916 44.94142, -93.32914 44.940...   \n",
       "7  MULTIPOLYGON (((-123.17382 37.77573, -123.1737...   \n",
       "8  POLYGON ((-77.11977 38.93428, -77.11900 38.933...   \n",
       "\n",
       "                                          place_name  bbox_north  bbox_south  \\\n",
       "0     Atlanta, Fulton County, Georgia, United States   33.886823   33.647808   \n",
       "1  Buffalo, Erie County, New York, United States ...   42.966469   42.826039   \n",
       "2    Cleveland, Cuyahoga County, Ohio, United States   41.604436   41.390628   \n",
       "3  Denver, Denver County, Colorado, United States...   39.914209   39.614315   \n",
       "4  Las Vegas, Clark County, Nevada, United States...   36.380491   36.129554   \n",
       "5  Miami, Miami-Dade County, Florida, United Stat...   25.855783   25.709052   \n",
       "6  Minneapolis, Hennepin County, Minnesota, Unite...   45.051250   44.890150   \n",
       "7  San Francisco, California, United States of Am...   37.929844   37.640314   \n",
       "8         Washington, D.C., United States of America   38.995852   38.791630   \n",
       "\n",
       "    bbox_east   bbox_west  \n",
       "0  -84.289560  -84.551068  \n",
       "1  -78.795168  -78.919453  \n",
       "2  -81.532744  -81.879094  \n",
       "3 -104.599689 -105.109885  \n",
       "4 -115.062066 -115.414628  \n",
       "5  -80.139157  -80.319760  \n",
       "6  -93.193858  -93.329163  \n",
       "7 -122.280016 -123.173825  \n",
       "8  -76.909366  -77.119766  "
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# verify OSMnx geocodes each query to what you expect (a [multi]polygon geometry)\n",
    "gdf = ox.gdf_from_places(places.values())\n",
    "gdf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Get the street networks and their edge bearings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def reverse_bearing(x):\n",
    "    return x + 180 if x < 180 else x - 180"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2019-12-04 11:07:04.150668 Atlanta\n",
      "2019-12-04 11:07:47.265704 Buffalo\n",
      "2019-12-04 11:07:57.895716 Cleveland\n",
      "2019-12-04 11:08:18.155257 Denver\n",
      "2019-12-04 11:09:10.610305 Las Vegas\n",
      "2019-12-04 11:09:53.734088 Miami\n",
      "2019-12-04 11:10:10.721075 Minneapolis\n",
      "2019-12-04 11:10:32.055285 San Francisco\n",
      "2019-12-04 11:10:59.697784 Washington\n"
     ]
    }
   ],
   "source": [
    "bearings = {}\n",
    "for place in sorted(places.keys()):\n",
    "    print(datetime.datetime.now(), place)\n",
    "    \n",
    "    # get the graph\n",
    "    query = places[place]\n",
    "    G = ox.graph_from_place(query, network_type='drive')\n",
    "    \n",
    "    # calculate edge bearings\n",
    "    Gu = ox.add_edge_bearings(ox.get_undirected(G))\n",
    "    \n",
    "    if weight_by_length:\n",
    "        # weight bearings by length (meters)\n",
    "        city_bearings = []\n",
    "        for u, v, k, d in Gu.edges(keys=True, data=True):\n",
    "            city_bearings.extend([d['bearing']] * int(d['length']))\n",
    "        b = pd.Series(city_bearings)\n",
    "        bearings[place] = pd.concat([b, b.map(reverse_bearing)]).reset_index(drop='True')\n",
    "    else:\n",
    "        # don't weight bearings, just take one value per street segment\n",
    "        b = pd.Series([d['bearing'] for u, v, k, d in Gu.edges(keys=True, data=True)])\n",
    "        bearings[place] = pd.concat([b, b.map(reverse_bearing)]).reset_index(drop='True')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Visualize it"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_and_merge(n, bearings):\n",
    "    # make twice as many bins as desired, then merge them in pairs\n",
    "    # prevents bin-edge effects around common values like 0° and 90°\n",
    "    n = n * 2\n",
    "    bins = np.arange(n + 1) * 360 / n\n",
    "    count, _ = np.histogram(bearings, bins=bins)\n",
    "    \n",
    "    # move the last bin to the front, so eg 0.01° and 359.99° will be binned together\n",
    "    count = np.roll(count, 1)\n",
    "    return count[::2] + count[1::2]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# function to draw a polar histogram for a set of edge bearings\n",
    "def polar_plot(ax, bearings, n=36, title=''):\n",
    "\n",
    "    bins = np.arange(n + 1) * 360 / n\n",
    "    count = count_and_merge(n, bearings)\n",
    "    _, division = np.histogram(bearings, bins=bins)\n",
    "    frequency = count / count.sum()\n",
    "    division = division[0:-1]\n",
    "    width =  2 * np.pi / n\n",
    "\n",
    "    ax.set_theta_zero_location('N')\n",
    "    ax.set_theta_direction('clockwise')\n",
    "\n",
    "    x = division * np.pi / 180\n",
    "    bars = ax.bar(x, height=frequency, width=width, align='center', bottom=0, zorder=2,\n",
    "                  color='#003366', edgecolor='k', linewidth=0.5, alpha=0.7)\n",
    "    \n",
    "    ax.set_ylim(top=frequency.max())\n",
    "    \n",
    "    title_font = {'family':'Century Gothic', 'size':24, 'weight':'bold'}\n",
    "    xtick_font = {'family':'Century Gothic', 'size':10, 'weight':'bold', 'alpha':1.0, 'zorder':3}\n",
    "    ytick_font = {'family':'Century Gothic', 'size': 9, 'weight':'bold', 'alpha':0.2, 'zorder':3}\n",
    "    \n",
    "    ax.set_title(title.upper(), y=1.05, fontdict=title_font)\n",
    "    \n",
    "    ax.set_yticks(np.linspace(0, max(ax.get_ylim()), 5))\n",
    "    yticklabels = ['{:.2f}'.format(y) for y in ax.get_yticks()]\n",
    "    yticklabels[0] = ''\n",
    "    ax.set_yticklabels(labels=yticklabels, fontdict=ytick_font)\n",
    "    \n",
    "    xticklabels = ['N', '', 'E', '', 'S', '', 'W', '']\n",
    "    ax.set_xticklabels(labels=xticklabels, fontdict=xtick_font)\n",
    "    ax.tick_params(axis='x', which='major', pad=-2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# create figure and axes\n",
    "n = len(places)\n",
    "ncols = int(np.ceil(np.sqrt(n)))\n",
    "nrows = int(np.ceil(n / ncols))\n",
    "figsize = (ncols * 5, nrows * 5)\n",
    "fig, axes = plt.subplots(nrows, ncols, figsize=figsize, subplot_kw={'projection':'polar'})\n",
    "\n",
    "# plot each city's polar histogram\n",
    "for ax, place in zip(axes.flat, sorted(places.keys())):\n",
    "    polar_plot(ax, bearings[place].dropna(), title=place)\n",
    "\n",
    "# add super title and save full image\n",
    "suptitle_font = {'family':'Century Gothic', 'fontsize':60, 'fontweight':'normal', 'y':1.07}\n",
    "fig.suptitle('City Street Network Orientation', **suptitle_font)\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(hspace=0.35)\n",
    "fig.savefig('images/street-orientations.png', dpi=120, bbox_inches='tight')\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python (ox)",
   "language": "python",
   "name": "ox"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
